<!DOCTYPE html>
<html lang='zh-CN'>

<head>
  <meta name="generator" content="Hexo 6.3.0">
  <meta name="hexo-theme" content="https://github.com/xaoxuu/hexo-theme-stellar/tree/1.19.0">
  <meta charset="utf-8">
  

  <meta http-equiv='x-dns-prefetch-control' content='on' />
  <link rel='dns-prefetch' href='https://gcore.jsdelivr.net'>
  <link rel="preconnect" href="https://gcore.jsdelivr.net" crossorigin>
  <link rel='dns-prefetch' href='//unpkg.com'>

  <meta name="renderer" content="webkit">
  <meta name="force-rendering" content="webkit">
  <meta http-equiv="X-UA-Compatible" content="IE=Edge,chrome=1">
  <meta name="HandheldFriendly" content="True" >
  <meta name="apple-mobile-web-app-capable" content="yes">
  <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
  <meta name="theme-color" content="#f8f8f8">
  
  <title>Gateway+JWT实现登录认证 - 愔颂</title>

  
    <meta name="description" content="微服务架构整合Spring Cloud Gateway网关和JWT Token实现登录认证。">
<meta property="og:type" content="article">
<meta property="og:title" content="Gateway+JWT实现登录认证">
<meta property="og:url" content="https://farhills.gitee.io/2023/10/05/Gateway+JWT%E5%AE%9E%E7%8E%B0%E7%99%BB%E5%BD%95%E8%AE%A4%E8%AF%81/index.html">
<meta property="og:site_name" content="愔颂">
<meta property="og:description" content="微服务架构整合Spring Cloud Gateway网关和JWT Token实现登录认证。">
<meta property="og:locale" content="zh_CN">
<meta property="og:image" content="https://cdn.nlark.com/yuque/0/2023/png/36098302/1695803598331-ae62b035-ebea-4bd1-a4ae-925d9d5fa3bf.png">
<meta property="og:image" content="https://cdn.nlark.com/yuque/0/2023/png/36098302/1695803973153-2016ad24-09e0-45a0-90ab-87f52cc8433b.png">
<meta property="og:image" content="https://cdn.nlark.com/yuque/0/2023/png/36098302/1695804115939-5a9bf903-76d2-40ea-bb8f-ff71f4331ead.png">
<meta property="og:image" content="https://cdn.nlark.com/yuque/0/2023/png/36098302/1695804211933-ba1c9872-9151-4b71-8a6e-bed30c472756.png">
<meta property="og:image" content="https://cdn.nlark.com/yuque/0/2023/png/36098302/1695805036897-0032b949-bd17-47f1-8f85-5cbfdcbcaf6e.png">
<meta property="og:image" content="https://cdn.nlark.com/yuque/0/2023/png/36098302/1695806266200-abb4a4b7-fa51-4070-bf5a-5339c77599c1.png">
<meta property="og:image" content="https://cdn.nlark.com/yuque/0/2023/png/36098302/1695806614615-47aa71bc-57ab-4508-ab3b-fb79a45f9551.png">
<meta property="og:image" content="https://cdn.nlark.com/yuque/0/2023/png/36098302/1695806760969-b89c771a-4804-46c9-90c7-ebe73bfab130.png">
<meta property="og:image" content="https://cdn.nlark.com/yuque/0/2023/png/36098302/1695806830484-93a15b7a-a18b-461d-8f1c-66a2a347355d.png">
<meta property="og:image" content="https://cdn.nlark.com/yuque/0/2023/png/36098302/1695807139680-723cc36a-b60e-4d53-a3a9-cbd659e0d896.png">
<meta property="article:published_time" content="2023-10-05T03:13:35.010Z">
<meta property="article:modified_time" content="2023-10-13T05:31:24.738Z">
<meta property="article:author" content="远岫">
<meta name="twitter:card" content="summary">
<meta name="twitter:image" content="https://cdn.nlark.com/yuque/0/2023/png/36098302/1695803598331-ae62b035-ebea-4bd1-a4ae-925d9d5fa3bf.png">
  
  
  
  

  <!-- feed -->
  

  
    
<link rel="stylesheet" href="/css/main.css">

  

  
    <link rel="shortcut icon" href="https://z1.ax1x.com/2023/10/05/pPXijyT.png">
  

  

  


  
</head>

<body>
  




  <div class='l_body' id='start'>
    <aside class='l_left' layout='post'>
    

  

<header class="header"><div class="logo-wrap"><a class="avatar" href="/about/"><div class="bg" style="opacity:0;background-image:url(https://gcore.jsdelivr.net/gh/cdn-x/placeholder@1.0.4/avatar/round/rainbow64@3x.webp);"></div><img no-lazy class="avatar" src="https://s1.ax1x.com/2022/11/12/ziJjfK.jpg" onerror="javascript:this.classList.add('error');this.src='https://gcore.jsdelivr.net/gh/cdn-x/placeholder@1.0.4/image/2659360.svg';"></a><a class="title" href="/"><div class="main" ff="title">愔颂</div></a></div>

<nav class="menu dis-select"><a class="nav-item active" href="/">文章</a><a class="nav-item" href="/friends/">收藏</a><a class="nav-item" href="/about/">关于</a></nav>
</header>


<div class="widgets">
<widget class="widget-wrapper search"><div class="widget-body"><div class="search-wrapper" id="search"><form class="search-form"><input type="text" class="search-input" id="search-input" data-filter="/blog/" placeholder="文章搜索"><svg t="1670596976048" class="icon search-icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2676" width="200" height="200"><path d="M938.2 832.6L723.8 618.1c-2.5-2.5-5.3-4.4-7.9-6.4 36.2-55.6 57.3-121.8 57.3-193.1C773.3 222.8 614.6 64 418.7 64S64 222.8 64 418.6c0 195.9 158.8 354.6 354.6 354.6 71.3 0 137.5-21.2 193.2-57.4 2 2.7 3.9 5.4 6.3 7.8L832.5 938c14.6 14.6 33.7 21.9 52.8 21.9 19.1 0 38.2-7.3 52.8-21.8 29.2-29.1 29.2-76.4 0.1-105.5M418.7 661.3C284.9 661.3 176 552.4 176 418.6 176 284.9 284.9 176 418.7 176c133.8 0 242.6 108.9 242.6 242.7 0 133.7-108.9 242.6-242.6 242.6" p-id="2677"></path></svg></form><div id="search-result"></div><div class="search-no-result">没有找到内容！</div></div></div></widget>


<widget class="widget-wrapper toc single" id="data-toc"><div class="widget-header cap dis-select"><span class="name">Gateway+JWT实现登录认证</span></div><div class="widget-body fs14"><div class="doc-tree active"><ol class="toc"><li class="toc-item toc-level-1"><a class="toc-link" href="#1%E3%80%81%E8%AE%A4%E8%AF%81%E3%80%81%E6%8E%88%E6%9D%83%E3%80%81%E5%87%AD%E8%AF%81"><span class="toc-text">1、认证、授权、凭证</span></a><ol class="toc-child"><li class="toc-item toc-level-2"><a class="toc-link" href="#1-1-%E8%AE%A4%E8%AF%81%EF%BC%88Authentication%EF%BC%89"><span class="toc-text">1.1 认证（Authentication）</span></a></li><li class="toc-item toc-level-2"><a class="toc-link" href="#1-2-%E6%8E%88%E6%9D%83%EF%BC%88Authorization%EF%BC%89"><span class="toc-text">1.2 授权（Authorization）</span></a></li><li class="toc-item toc-level-2"><a class="toc-link" href="#1-3-%E5%87%AD%E8%AF%81%EF%BC%88Credential%EF%BC%89"><span class="toc-text">1.3 凭证（Credential）</span></a><ol class="toc-child"><li class="toc-item toc-level-3"><a class="toc-link" href="#1-3-1%E3%80%81Cookie-Session-%E6%A8%A1%E5%BC%8F"><span class="toc-text">1.3.1、Cookie-Session 模式</span></a></li><li class="toc-item toc-level-3"><a class="toc-link" href="#1-3-2%E3%80%81JWT%E6%96%B9%E6%A1%88"><span class="toc-text">1.3.2、JWT方案</span></a></li></ol></li></ol></li><li class="toc-item toc-level-1"><a class="toc-link" href="#2%E3%80%81%E8%AE%A4%E8%AF%81%E5%8E%9F%E7%90%86%E5%9B%BE"><span class="toc-text">2、认证原理图</span></a></li><li class="toc-item toc-level-1"><a class="toc-link" href="#3%E3%80%81%E5%AE%9E%E7%8E%B0%E7%BB%86%E8%8A%82"><span class="toc-text">3、实现细节</span></a><ol class="toc-child"><li class="toc-item toc-level-2"><a class="toc-link" href="#3-1%E3%80%81%E5%A6%82%E4%BD%95%E5%81%9A%E7%99%BB%E5%BD%95%E8%AE%A4%E8%AF%81"><span class="toc-text">3.1、如何做登录认证</span></a></li><li class="toc-item toc-level-2"><a class="toc-link" href="#3-2%E3%80%81%E5%A6%82%E4%BD%95%E7%94%9F%E6%88%90%E4%BB%A4%E7%89%8C"><span class="toc-text">3.2、如何生成令牌</span></a></li><li class="toc-item toc-level-2"><a class="toc-link" href="#3-3%E3%80%81%E5%A6%82%E4%BD%95%E6%90%BA%E5%B8%A6JWT%E5%8F%91%E9%80%81%E8%AF%B7%E6%B1%82"><span class="toc-text">3.3、如何携带JWT发送请求</span></a></li><li class="toc-item toc-level-2"><a class="toc-link" href="#3-4%E3%80%81%E7%BD%91%E5%85%B3%E5%A6%82%E4%BD%95%E9%AA%8C%E8%AF%81%E5%92%8C%E8%BD%AC%E5%8F%91%E8%AF%B7%E6%B1%82"><span class="toc-text">3.4、网关如何验证和转发请求</span></a></li><li class="toc-item toc-level-2"><a class="toc-link" href="#3-5%E3%80%81%E4%BC%9A%E5%91%98%E4%B8%9A%E5%8A%A1%E9%80%BB%E8%BE%91%E5%A4%84%E7%90%86"><span class="toc-text">3.5、会员业务逻辑处理</span></a></li><li class="toc-item toc-level-2"><a class="toc-link" href="#3-6%E3%80%81%E5%A6%82%E4%BD%95%E5%88%B7%E6%96%B0%E4%BB%A4%E7%89%8C"><span class="toc-text">3.6、如何刷新令牌</span></a></li></ol></li></ol></div></div></widget>




</div>


    </aside>
    <div class='l_main'>
      

      



<div class="bread-nav fs12"><div id="breadcrumb"><a class="cap breadcrumb" href="/">主页</a><span class="sep"></span><a class="cap breadcrumb" href="/">文章</a><span class="sep"></span><a class="cap breadcrumb-link" href="/categories/%E9%A1%B9%E7%9B%AE%E5%AE%9E%E6%88%98/">项目实战</a></div><div id="post-meta">发布于&nbsp;<time datetime="2023-10-05T03:13:35.010Z">2023-10-05</time></div></div>

<article class='md-text content post'>
<h1 class="article-title"><span>Gateway+JWT实现登录认证</span></h1>
<meta name="referrer" content="no-referrer"/>

<p>微服务架构整合Spring Cloud Gateway网关和JWT Token实现登录认证。</p>
<span id="more"></span>

<h1 id="1、认证、授权、凭证"><a href="#1、认证、授权、凭证" class="headerlink" title="1、认证、授权、凭证"></a>1、认证、授权、凭证</h1><h2 id="1-1-认证（Authentication）"><a href="#1-1-认证（Authentication）" class="headerlink" title="1.1 认证（Authentication）"></a>1.1 认证（Authentication）</h2><p>认证表示你是谁。系统如何正确分辨出操作用户的真实身份，比如通过输入用户名和密码来辨别身份。</p>
<h2 id="1-2-授权（Authorization）"><a href="#1-2-授权（Authorization）" class="headerlink" title="1.2 授权（Authorization）"></a>1.2 授权（Authorization）</h2><p>授权表示你能干什么。系统如何控制一个用户能看到哪些数据和操作哪些功能，也就是具有哪些权限。</p>
<h2 id="1-3-凭证（Credential）"><a href="#1-3-凭证（Credential）" class="headerlink" title="1.3 凭证（Credential）"></a>1.3 凭证（Credential）</h2><p>表示你如何证明你的身份。系统如何保证它与用户之间的承诺是双方当时真实意图的体现，是准确、完整和不可抵赖的。</p>
<p>关于凭证的存储方案，业界的安全架构中有两种方案：</p>
<ol>
<li>Cookie-Session 模式</li>
<li>JWT 方案</li>
</ol>
<h3 id="1-3-1、Cookie-Session-模式"><a href="#1-3-1、Cookie-Session-模式" class="headerlink" title="1.3.1、Cookie-Session 模式"></a>1.3.1、Cookie-Session 模式</h3><p>如下图示：</p>
<div class="tag-plugin image"><div class="image-bg"><img src="https://cdn.nlark.com/yuque/0/2023/png/36098302/1695803598331-ae62b035-ebea-4bd1-a4ae-925d9d5fa3bf.png" fancybox="true"/></div></div>

<p><strong>优点：</strong></p>
<p>状态信息都存储于服务器，只要依靠客户端的<a target="_blank" rel="noopener" href="https://en.wikipedia.org/wiki/Same-origin_policy">同源策略</a>和 HTTPS 的传输层安全，保证 Cookie 中的键值不被窃取而出现被冒认身份的情况，就能完全规避掉上下文信息在传输过程中被泄漏和篡改的风险。</p>
<p><strong>缺点：</strong></p>
<p>在单节点的单体服务中再适合不过，但是如果需要水平扩展要部署集群就很麻烦。</p>
<p>如果让 session 分配到不同的的节点上，不重复地保存着一部分用户的状态，用户的请求固定分配到对应的节点上，如果某个节点崩溃了，则里面的用户状态就会完全丢失。如果让 session 复制到所有节点上，那么同步的成本又会很高。</p>
<h3 id="1-3-2、JWT方案"><a href="#1-3-2、JWT方案" class="headerlink" title="1.3.2、JWT方案"></a>1.3.2、JWT方案</h3><p>上面说到 Cookie-Session 机制在分布式环境下会遇到一致性和同步成本的问题，而且如果在多方系统中，则更不能将 Session 共享存放在多方系统的服务端中，即使服务端之间能共享数据，Cookie 也没有办法跨域。</p>
<p>转换思路，服务端不保存任何状态信息，由客户端来存储，每次发送请求时携带这个状态信息发给后端服务。原理图如下所示：</p>
<div class="tag-plugin image"><div class="image-bg"><img src="https://cdn.nlark.com/yuque/0/2023/png/36098302/1695803973153-2016ad24-09e0-45a0-90ab-87f52cc8433b.png" fancybox="true"/></div></div>

<p>JWT（JSON WEB TOKEN）是一种令牌格式，经常与 OAuth2.0 配合应用于分布式、多方的应用系统中。</p>
<p>我们先来看下 JWT 的格式长什么样：</p>
<div class="tag-plugin image"><div class="image-bg"><img src="https://cdn.nlark.com/yuque/0/2023/png/36098302/1695804115939-5a9bf903-76d2-40ea-bb8f-ff71f4331ead.png" fancybox="true"/></div></div>

<p>左边的字符串就是 JWT 令牌，JWT 令牌是服务端生成的，客户端会拿着这个 JWT 令牌在每次发送请求时放到 HTTP header 中。</p>
<p>而右边是 JWT 经过 Base64 解码后展示的明文内容，而这段明文内容的最下方，又有一个签名内容，可以防止内容篡改，但是不能解决泄漏的问题。</p>
<p><a target="_blank" rel="noopener" href="http://www.passjava.cn/#/02.SpringCloud/06.Gateway%E7%BD%91%E5%85%B3/03.%E5%94%90%E7%8E%84%E5%A5%98%E6%8A%8AJWT%E4%BB%A4%E7%89%8C%E7%8E%A9%E5%88%B0%E4%BA%86%E6%9C%BA%E5%88%B6?id=jwt-%E6%A0%BC%E5%BC%8F"><strong>JWT 格式</strong></a></p>
<p>JWT 令牌是以 JSON 结构存储，用点号分割为三个部分。</p>
<div class="tag-plugin image"><div class="image-bg"><img src="https://cdn.nlark.com/yuque/0/2023/png/36098302/1695804211933-ba1c9872-9151-4b71-8a6e-bed30c472756.png" fancybox="true"/></div></div>

<p>第一部分是<strong>令牌头</strong>（Header），内容如下所示：</p>
<figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">&#123;</span><br><span class="line">  &quot;alg&quot;: &quot;HS256&quot;,</span><br><span class="line">  &quot;typ&quot;: &quot;JWT&quot;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<p>它描述了令牌的类型（统一为 typ:JWT）以及令牌签名的算法，示例中 HS256 为 HMAC SHA256 算法的缩写<a target="_blank" rel="noopener" href="https://jwt.io/%E7%BD%91%E7%AB%99%E6%89%80%E5%88%97%E3%80%82">。</a></p>
<p>令牌的第二部分是<strong>负载</strong>（Payload），这是令牌是真正需要向服务端传递的信息。但是服务端不会直接用这个负载，而是通过加密传过来的 Header 和 Payload 后再比对签名是否一致来判断负载是否被篡改，如果没有被篡改，才能用 Payload 中的内容。因为负载只是做了 base64 编码，并不是加密，所以是不安全的，千万别把敏感信息比如密码放到负载里面。</p>
<figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">&#123;</span><br><span class="line">  &quot;sub&quot;: &quot;passjava&quot;,</span><br><span class="line">  &quot;name&quot;: &quot;悟空聊架构&quot;,</span><br><span class="line">  &quot;iat&quot;: 1516239022</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>



<p>令牌的第三部分是<strong>签名</strong>（Signature），使用在对象头中公开的特定签名算法，通过特定的密钥（Secret，由服务器进行保密，不能公开）对前面两部分内容进行加密计算，以例子里使用的 JWT 默认的 HMAC SHA256 算法为例，将通过以下公式产生签名值：</p>
<figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">HMACSHA256(base64UrlEncode(header) + &quot;.&quot; + base64UrlEncode(payload) , secret)</span><br></pre></td></tr></table></figure>

<p><strong>签名的意义</strong>：确保负载中的信息是可信的、没有被篡改的，也没有在传输过程中丢失任何信息。因为被签名的内容哪怕发生了一个字节的变动，也会导致整个签名发生显著变化。此外，由于签名这件事情只能由认证授权服务器完成（只有它知道 Secret），任何人都无法在篡改后重新计算出合法的签名值，所以服务端才能够完全信任客户端传上来的 JWT 中的负载信息。</p>
<p><strong>JWT 的优势</strong></p>
<ul>
<li><strong>无状态</strong>：不需要服务端保存 JWT 令牌，也就是说不需要服务节点保留任何一点状态信息，就能在后续的请求中完成认证功能。</li>
<li><strong>天然的扩容便利</strong>：服务做水平扩容不用考虑 JWT 令牌，而 Cookie-Session 是需要考虑扩容后服务节点如何存储 Session 的。</li>
<li><strong>不依赖 Cookie</strong>：JWT 可以存放在浏览器的 LocalStorage，不一定非要存储在 Cookie 中。</li>
</ul>
<p><strong>JWT 的劣势</strong></p>
<ul>
<li><strong>令牌难以主动失效</strong>：JWT 令牌签发后，理论上和认证的服务器就没有什么关系了，到期之前始终有效。除非服务器加些特殊的逻辑处理来缓存 JWT，并来管理 JWT 的生命周期，但是这种方式又会退化成有状态服务。而这种要求有状态的需求又很常见：譬如用户退出后，需要重新输入用户名和密码才能登录；或者用户只允许在一台设备登录，登录到另外一台设备，要求强行退出。但是这种有状态的模式，降低了 JWT 本身的价值。</li>
<li><strong>更容易遭受重放攻击</strong>：Cookie-Session 也有重放攻击的问题，也就是客户端可以拿着这个 cookie 不断发送大量请求，对系统性能造成影响。但是因为 Session 在服务端也有一份，服务端可以控制 session 的生命周期，应对重放攻击更加主动一些。但是 JWT 的重放攻击对于服务端来说就很被动，比如通过客户端的验证码、服务端限流或者缩短令牌有效期，应用起来都会麻烦些。</li>
<li><strong>存在泄漏的风险</strong>：客户端存储，很有可能泄漏出去，被其他人重复利用。</li>
<li><strong>信息大小有限</strong>：HTTP 协议并没有强制约束 Header 的最大长度，但是服务器、浏览器会做限制。而且如果令牌很大还会消耗传输带宽。</li>
</ul>
<h1 id="2、认证原理图"><a href="#2、认证原理图" class="headerlink" title="2、认证原理图"></a>2、认证原理图</h1><p>在如下的认证时序图中，有以下几种角色：</p>
<ul>
<li><strong>客户端</strong>：表示 APP 端或 PC 端的前端页面。</li>
<li><strong>网关</strong>：表示 Spring Cloud Gateway 网关服务。</li>
<li><strong>认证服务</strong>：用来接收客户的登录请求、登出请求、刷新令牌的操作。</li>
<li><strong>业务服务</strong>：和系统业务相关的微服务。</li>
</ul>
<p>认证和校验身份的流程如下所示：</p>
<div class="tag-plugin image"><div class="image-bg"><img src="https://cdn.nlark.com/yuque/0/2023/png/36098302/1695805036897-0032b949-bd17-47f1-8f85-5cbfdcbcaf6e.png" fancybox="true"/></div></div>

<p>① <strong>用户登录</strong>：客户端在登录页面输入用户名和密码，提交表单，调用登录接口。</p>
<p>② <strong>转发请求</strong>：这里会先将登录请求发送到网关服务 passjava-gateway，网关对于登录请求会直接转发到认证服务 passjava-auth。（网关对登录请求不做 token 校验，这个可以配置不校验哪些请求 URL）</p>
<p>③ <strong>认证</strong>：认证服务会将请求参数中的用户名+密码和数据库中的用户进行比对，如果完全匹配，则认证通过。</p>
<p>④ <strong>生成令牌</strong>：生成两个令牌：access_token 和 refresh_token（刷新令牌），刷新令牌我们后面再说，这里其实也可以只用生成一个令牌 access_token。令牌里面会包含用户的身份信息，<strong>如果要做权限管控</strong>，还需要在 token 里面包含用户的权限信息，权限这一块不在本篇展开，会放到下一篇中进行讲解。</p>
<p>⑤ <strong>客户端缓存 token</strong>：客户端拿到两个 token 缓存到 cookie 中或者 LocalStorage 中。</p>
<p>⑥ <strong>携带 token 发起请求</strong>：客户端下次想调用业务服务时，将 access_token 放到请求的 header 中。</p>
<p>⑦ <strong>网关校验 token</strong>：请求还是先到到网关服务，然后由它校验 access_token 是否合法。如果 access_token 未过期，且能正确解析出来，就说明是合法的 access_token。</p>
<p>⑧ <strong>携带用户身份信息转发请求</strong>：网关将 access_token 中携带的用户的 user_id 放到请求的 header 中，转发给真正的业务服务。</p>
<p>⑨ <strong>处理业务逻辑</strong>：业务服务从 header 中拿到用户的 user_id，然后处理业务逻辑，处理完后将结果延原理返回给客户端。</p>
<h1 id="3、实现细节"><a href="#3、实现细节" class="headerlink" title="3、实现细节"></a>3、实现细节</h1><p>更多细节见：<a target="_blank" rel="noopener" href="http://www.passjava.cn/#/02.SpringCloud/06.Gateway%E7%BD%91%E5%85%B3/04.%E5%AE%9E%E6%88%98SpringCloud+JWT%E8%AE%A4%E8%AF%81">实战SpringCloud gateway+JWT认证</a></p>
<h2 id="3-1、如何做登录认证"><a href="#3-1、如何做登录认证" class="headerlink" title="3.1、如何做登录认证"></a>3.1、如何做登录认证</h2> <div class="tag-plugin image"><div class="image-bg"><img src="https://cdn.nlark.com/yuque/0/2023/png/36098302/1695806266200-abb4a4b7-fa51-4070-bf5a-5339c77599c1.png" fancybox="true"/></div></div>

<p>步骤：</p>
<ol>
<li>提交用户名和密码</li>
<li>网关服务转发登录请求</li>
<li>数据库查找用户密码，验证成功后生成JWT令牌</li>
</ol>
<h2 id="3-2、如何生成令牌"><a href="#3-2、如何生成令牌" class="headerlink" title="3.2、如何生成令牌"></a>3.2、如何生成令牌</h2><p>生成令牌就是通过工具类 PassJavaJwtTokenUtil 生成 JWT Token。</p>
<h2 id="3-3、如何携带JWT发送请求"><a href="#3-3、如何携带JWT发送请求" class="headerlink" title="3.3、如何携带JWT发送请求"></a>3.3、如何携带JWT发送请求</h2><div class="tag-plugin image"><div class="image-bg"><img src="https://cdn.nlark.com/yuque/0/2023/png/36098302/1695806614615-47aa71bc-57ab-4508-ab3b-fb79a45f9551.png" fancybox="true"/></div></div>

<p>客户端（浏览器或 APP）拿到 JWT 后，可以将 JWT 存放在浏览器的 Cookie 或 LocalStorage（本地存储） 或者内存中。</p>
<p>发送请求时在请求 Header 的 Authorization 字段中设置 JWT。</p>
<h2 id="3-4、网关如何验证和转发请求"><a href="#3-4、网关如何验证和转发请求" class="headerlink" title="3.4、网关如何验证和转发请求"></a>3.4、网关如何验证和转发请求</h2><div class="tag-plugin image"><div class="image-bg"><img src="https://cdn.nlark.com/yuque/0/2023/png/36098302/1695806760969-b89c771a-4804-46c9-90c7-ebe73bfab130.png" fancybox="true"/></div></div>

<p>网关接收到前端发起的业务请求后，会先验证请求的 Header 中是否携带 Authorization 字段，以及里面的 Token 是否合法。然后解析 Token 中的 userId 和 username，放到 header 中再进行转发。</p>
<p>网关是通过多个过滤器 Filter对请求进行串行拦截处理的，所以我们可以自定义一个全局过滤器，对所有请求进行校验，当然对于一些特殊请求比如登录请求就不需要校验了，因为调用登录请求的时候还没有生成 Token。</p>
<div class="tag-plugin image"><div class="image-bg"><img src="https://cdn.nlark.com/yuque/0/2023/png/36098302/1695806830484-93a15b7a-a18b-461d-8f1c-66a2a347355d.png" fancybox="true"/></div></div>



<h2 id="3-5、会员业务逻辑处理"><a href="#3-5、会员业务逻辑处理" class="headerlink" title="3.5、会员业务逻辑处理"></a>3.5、会员业务逻辑处理</h2><p>会员服务接收到网关转发的请求后，就从 Header 中拿到用户身份信息，然后通过 userId 获取会员信息。</p>
<p>获取 userId 的方式其实可以通过加一个拦截器，由拦截器将 Header 中的 userId 和 username 放到线程中，后续的 controller，service，dao 类都可以从线程里面拿到 userId 和 username，不用通过传参的方式。</p>
<p>获取 userId 的方式：</p>
<ul>
<li>方式一：从 request 的 Header 中拿到 userId。代码简单，但是如果其他地方也要用到 userId，则需要通过方法传参的方式传递 userId。</li>
<li>方式二：从线程变量里面拿到 userId。代码复杂，使用简单。好处是所有地方统一从一个地方获取。</li>
</ul>
<h2 id="3-6、如何刷新令牌"><a href="#3-6、如何刷新令牌" class="headerlink" title="3.6、如何刷新令牌"></a>3.6、如何刷新令牌</h2><p>当认证服务返回给客户端的 JWT 也就是 access_token 过期后，客户端是通过发送登录请求重新拿到 access_token 吗？</p>
<p>这种重新登录的操作如果很频繁（因 JWT 过期时间较短），对于用户来说体验就很差了。客户端需要跳转到登录页面，让用户重新提交用户名和密码，即使客户端有记住用户名和密码，但是这种跳转的到登录页的操作会大幅度降低用户的体验。</p>
<p><strong>有没有一种比较优雅的方式让客户端重新拿到 access_token 或者说延长 access_token 有效期呢？</strong></p>
<p>我们知道 JWT 生成后是不能篡改里面的内容，即使是 JWT 的有效期也不行。所以延长 access_token 有效期的做法并不适合，而且如果长期保持一个 access_token 有效，也是不安全的。</p>
<p>那就只能重新生成 access_token 了。方案其实挺简单，客户端拿之前生成的 JWT 调用后端一个接口，然后后端校验这个 JWT 是否合法，如果是合法的就重新生成一个新的返回给客户端。客户端自行替换掉之前本地保存的 access_token 就可以了。</p>
<div class="tag-plugin image"><div class="image-bg"><img src="https://cdn.nlark.com/yuque/0/2023/png/36098302/1695807139680-723cc36a-b60e-4d53-a3a9-cbd659e0d896.png" fancybox="true"/></div></div>

<p>这里有一个巧妙的设计，就是生成 JWT 时，返回了两个 JWT token，一个 access_token，一个 refresh_token，这两个 token 其实都可以用来刷新 token，但是我们把 refresh_token 设置的过期时间稍微长一点，比如两倍于 access_token，当 access_token 过期后，refresh_token 如果还没有过期，<strong>就可以利用两者的过期时间差进行重新生成令牌的操作</strong>，也就是刷新令牌，这里的刷新指的是客户端重置本地保存的令牌，以后都用新的令牌。</p>
<p>总结：</p>
<p>在登录认证中，刷新令牌和设置过期重登是常见的安全机制，用于保证用户的登录状态和防止令牌被滥用。下面是一种常见的实现方式：</p>
<ol>
<li><p>生成令牌和刷新令牌：</p>
<p>- 在用户登录成功后，生成一个访问令牌（access token）和一个刷新令牌（refresh token）。</p>
<p>- 访问令牌用于验证用户的身份和访问权限，通常具有较短的有效期。</p>
<p>- 刷新令牌用于在访问令牌过期时获取新的访问令牌，通常具有较长的有效期。</p>
</li>
<li><p>访问令牌的验证：</p>
<p>- 在每个请求中，服务端需要验证访问令牌的有效性。</p>
<p>- 可以通过在请求头或请求参数中携带访问令牌，并在服务端进行解析和验证。</p>
<p>- 如果访问令牌已过期或无效，服务端会返回相应的错误响应。</p>
</li>
<li><p>刷新令牌的使用：</p>
<p>- 当访问令牌过期时，客户端可以使用刷新令牌来获取新的访问令牌。</p>
<p>- 客户端发送一个特殊的请求，携带刷新令牌。</p>
<p>- 服务端验证刷新令牌的有效性，并根据刷新令牌颁发一个新的访问令牌。</p>
</li>
<li><p>设置过期重登：</p>
<p>- 如果用户长时间不活动或注销登录，访问令牌会过期失效。</p>
<p>- 当用户再次访问需要登录的接口时，服务端会返回一个需要重新登录的错误响应。</p>
<p>- 客户端收到该错误响应后，需要引导用户重新进行登录认证。</p>
</li>
</ol>
<p>通过刷新令牌和设置过期重登的机制，可以在保证用户登录状态的同时，提高系统的安全性。请注意，具体的实现方式可能会根据实际情况和框架的不同而有所差异。</p>





</article>

<div class="related-wrap reveal" id="read-next"><section class="body"><div class="item" id="prev"><div class="note">较新文章</div><a href="/2023/10/05/MyBatis-Plus%E6%95%B4%E5%90%88%E7%AC%94%E8%AE%B0/">MyBatis-Plus整合笔记</a></div><div class="item" id="next"><div class="note">较早文章</div><a href="/2023/10/05/Easy-RPC%E6%A1%86%E6%9E%B6%E5%AE%9E%E7%8E%B0/">Easy-RPC框架实现</a></div></section></div>






  <div class='related-wrap md-text reveal' id="comments">
    <section class='header cmt-title cap theme'>
      快来参与讨论吧
    </section>
    <section class='body cmt-body giscus'>
      

<svg class="loading" style="vertical-align: middle;fill: currentColor;overflow: hidden;" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2709"><path d="M832 512c0-176-144-320-320-320V128c211.2 0 384 172.8 384 384h-64zM192 512c0 176 144 320 320 320v64C300.8 896 128 723.2 128 512h64z" p-id="2710"></path></svg>

<div id="giscus" data-repo="echoalways/giscus" data-repo-id="R_kgDOKjJRiQ" data-category="Announcements" data-category-id="DIC_kwDOKjJRic4CaUxu" data-mapping="pathname" data-strict="0" data-reactions-enabled="1" data-emit-metadata="0" data-input-position="top" data-theme="preferred_color_scheme" data-lang="zh-CN" data-loading="lazy" crossorigin="anonymous"></div>

    </section>
  </div>



      
<footer class="page-footer reveal fs12"><hr><div class="text" style="text-align:center;"><p>云无心以出岫@远岫♥</p>
<div><span id="timeDate">载入天数...</span><span id="times">载入时分秒...</span>，<span id="busuanzi_container_site_pv">总访问量: <span id="busuanzi_value_site_pv"></span>次</span>，<span id="busuanzi_container_site_uv">访客数: <span id="busuanzi_value_site_uv"></span>人</span></div></div></footer>

<script async src="//busuanzi.ibruce.info/busuanzi/2.3/busuanzi.pure.mini.js"></script>
<script>
  function createtime() {
    var now = new Date();
    var grt= new Date("10/03/2023 18:40:00");
    now.setTime(now.getTime()+250);
    days = (now - grt ) / 1000 / 60 / 60 / 24; dnum = Math.floor(days);
    hours = (now - grt ) / 1000 / 60 / 60 - (24 * dnum); hnum = Math.floor(hours);
    if(String(hnum).length ==1 ){hnum = "0" + hnum;} minutes = (now - grt ) / 1000 /60 - (24 * 60 * dnum) - (60 * hnum);
    mnum = Math.floor(minutes); if(String(mnum).length ==1 ){mnum = "0" + mnum;}
    seconds = (now - grt ) / 1000 - (24 * 60 * 60 * dnum) - (60 * 60 * hnum) - (60 * mnum);
    snum = Math.round(seconds); if(String(snum).length ==1 ){snum = "0" + snum;}
    document.getElementById("timeDate").innerHTML = "本站已运行 "+dnum+" 天 ";
    document.getElementById("times").innerHTML = hnum + " 小时 " + mnum + " 分 " + snum + " 秒";
  };
  setInterval("createtime()",250);
</script>

      <div class='float-panel mobile-only blur' style='display:none'>
  <button type='button' class='sidebar-toggle mobile' onclick='sidebar.toggle()'>
    <svg class="icon" style="width: 1em; height: 1em;vertical-align: middle;fill: currentColor;overflow: hidden;" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="15301"><path d="M566.407 808.3c26.9-0.1 49.3-20.8 51.6-47.6-1.9-27.7-23.9-49.7-51.6-51.6h-412.6c-28.2-1.4-52.6 19.5-55.5 47.6 2.3 26.8 24.6 47.5 51.6 47.6h416.5v4z m309.3-249.9c26.9-0.1 49.3-20.8 51.6-47.6-2.2-26.8-24.6-47.5-51.6-47.6h-721.9c-27.7-2.8-52.5 17.4-55.3 45.1-0.1 0.8-0.1 1.7-0.2 2.5 0.9 27.2 23.6 48.5 50.7 47.6H875.707z m-103.1-245.9c26.9-0.1 49.3-20.8 51.6-47.6-0.4-28.3-23.2-51.1-51.5-51.6h-618.9c-29.5-1.1-54.3 21.9-55.5 51.4v0.2c1.4 27.8 25.2 49.2 53 47.8 0.8 0 1.7-0.1 2.5-0.2h618.8z" p-id="15302"></path><path d="M566.407 808.3c26.9-0.1 49.3-20.8 51.6-47.6-1.9-27.7-23.9-49.7-51.6-51.6h-412.6c-28.2-1.4-52.6 19.5-55.5 47.6 1.9 27.7 23.9 49.7 51.6 51.6h416.5z m309.3-249.9c26.9-0.1 49.3-20.8 51.6-47.6-2.2-26.8-24.6-47.5-51.6-47.6h-721.9c-27.7-2.8-52.5 17.4-55.3 45.1-0.1 0.8-0.1 1.7-0.2 2.5 0.9 27.2 23.6 48.5 50.7 47.6H875.707z m-103.1-245.9c26.9-0.1 49.3-20.8 51.6-47.6-0.4-28.3-23.2-51.1-51.5-51.6h-618.9c-29.5-1.1-54.3 21.9-55.5 51.4v0.2c1.4 27.8 25.2 49.2 53 47.8 0.8 0 1.7-0.1 2.5-0.2h618.8z" p-id="15303"></path></svg>
  </button>
</div>

    </div>
  </div>
  <div class='scripts'>
    <script type="text/javascript">
  const stellar = {
    // 懒加载 css https://github.com/filamentgroup/loadCSS
    loadCSS: (href, before, media, attributes) => {
      var doc = window.document;
      var ss = doc.createElement("link");
      var ref;
      if (before) {
        ref = before;
      } else {
        var refs = (doc.body || doc.getElementsByTagName("head")[0]).childNodes;
        ref = refs[refs.length - 1];
      }
      var sheets = doc.styleSheets;
      if (attributes) {
        for (var attributeName in attributes) {
          if (attributes.hasOwnProperty(attributeName)) {
            ss.setAttribute(attributeName, attributes[attributeName]);
          }
        }
      }
      ss.rel = "stylesheet";
      ss.href = href;
      ss.media = "only x";
      function ready(cb) {
        if (doc.body) {
          return cb();
        }
        setTimeout(function () {
          ready(cb);
        });
      }
      ready(function () {
        ref.parentNode.insertBefore(ss, before ? ref : ref.nextSibling);
      });
      var onloadcssdefined = function (cb) {
        var resolvedHref = ss.href;
        var i = sheets.length;
        while (i--) {
          if (sheets[i].href === resolvedHref) {
            return cb();
          }
        }
        setTimeout(function () {
          onloadcssdefined(cb);
        });
      };
      function loadCB() {
        if (ss.addEventListener) {
          ss.removeEventListener("load", loadCB);
        }
        ss.media = media || "all";
      }
      if (ss.addEventListener) {
        ss.addEventListener("load", loadCB);
      }
      ss.onloadcssdefined = onloadcssdefined;
      onloadcssdefined(loadCB);
      return ss;
    },

    // 从 butterfly 和 volantis 获得灵感
    loadScript: (src, opt) => new Promise((resolve, reject) => {
      var script = document.createElement('script');
      if (src.startsWith('/')){
        src = stellar.config.root + src.substring(1);
      }
      script.src = src;
      if (opt) {
        for (let key of Object.keys(opt)) {
          script[key] = opt[key]
        }
      } else {
        // 默认异步，如果需要同步，第二个参数传入 {} 即可
        script.async = true
      }
      script.onerror = reject
      script.onload = script.onreadystatechange = function() {
        const loadState = this.readyState
        if (loadState && loadState !== 'loaded' && loadState !== 'complete') return
        script.onload = script.onreadystatechange = null
        resolve()
      }
      document.head.appendChild(script)
    }),

    // https://github.com/jerryc127/hexo-theme-butterfly
    jQuery: (fn) => {
      if (typeof jQuery === 'undefined') {
        stellar.loadScript(stellar.plugins.jQuery).then(fn)
      } else {
        fn()
      }
    }
  };
  stellar.version = '1.19.0';
  stellar.github = 'https://github.com/xaoxuu/hexo-theme-stellar/tree/1.19.0';
  stellar.config = {
    date_suffix: {
      just: '刚刚',
      min: '分钟前',
      hour: '小时前',
      day: '天前',
      month: '个月前',
    },
    root : '/',
  };

  // required plugins (only load if needs)
  stellar.plugins = {
    jQuery: 'https://gcore.jsdelivr.net/npm/jquery@3.6.2/dist/jquery.min.js'
  };

  if ('local_search') {
    stellar.search = {};
    stellar.search.service = 'local_search';
    if (stellar.search.service == 'local_search') {
      let service_obj = Object.assign({}, {"field":"all","path":"/search.json","content":true,"sort":"-date"});
      stellar.search[stellar.search.service] = service_obj;
    }
  }

  // stellar js
  stellar.plugins.stellar = Object.assign({"sites":"/js/plugins/sites.js","friends":"/js/plugins/friends.js","ghinfo":"/js/plugins/ghinfo.js","timeline":"/js/plugins/timeline.js","linkcard":"/js/plugins/linkcard.js","fcircle":"/js/plugins/fcircle.js","weibo":"/js/plugins/weibo.js"});

  stellar.plugins.marked = Object.assign("https://cdn.bootcdn.net/ajax/libs/marked/4.0.18/marked.min.js");
  // optional plugins
  if ('false' == 'true') {
    stellar.plugins.lazyload = Object.assign({"enable":false,"js":"https://gcore.jsdelivr.net/npm/vanilla-lazyload@17.8.3/dist/lazyload.min.js","transition":"blur"});
  }
  if ('true' == 'true') {
    stellar.plugins.swiper = Object.assign({"enable":true,"css":"https://unpkg.com/swiper@8.4.5/swiper-bundle.min.css","js":"https://unpkg.com/swiper@8.4.5/swiper-bundle.min.js"});
  }
  if ('' == 'true') {
    stellar.plugins.scrollreveal = Object.assign({"enable":null,"js":"https://gcore.jsdelivr.net/npm/scrollreveal@4.0.9/dist/scrollreveal.min.js","distance":"8px","duration":500,"interval":100,"scale":1});
  }
  if ('true' == 'true') {
    stellar.plugins.preload = Object.assign({"enable":true,"service":"flying_pages","instant_page":"https://gcore.jsdelivr.net/gh/volantis-x/cdn-volantis@4.1.2/js/instant_page.js","flying_pages":"https://gcore.jsdelivr.net/gh/gijo-varghese/flying-pages@2.1.2/flying-pages.min.js"});
  }
  if ('true' == 'true') {
    stellar.plugins.fancybox = Object.assign({"enable":true,"js":"https://gcore.jsdelivr.net/npm/@fancyapps/ui@4.0/dist/fancybox.umd.js","css":"https://gcore.jsdelivr.net/npm/@fancyapps/ui@4.0/dist/fancybox.css","selector":".swiper-slide img"});
  }
  if ('false' == 'true') {
    stellar.plugins.heti = Object.assign({"enable":false,"css":"https://unpkg.com/heti@0.9.2/umd/heti.min.css","js":"https://unpkg.com/heti@0.9.2/umd/heti-addon.min.js"});
  }
  if ('true' == 'true') {
    stellar.plugins.copycode = Object.assign({"enable":true,"js":"/js/plugins/copycode.js","default_text":"Copy","success_text":"Copied"});
  }
</script>

<!-- required -->

  
<script src="/js/main.js" async></script>



<!-- optional -->

  <script>
  function loadJS() {
    const els = document.querySelectorAll("#comments #giscus");
    if (els.length === 0) return;
    els.forEach((el, i) => {
      try {
        el.innerHTML = '';
      } catch (error) {
        console.log(error);
      }
      var script = document.createElement('script');
      script.src = 'https://giscus.app/client.js';
      script.async = true;
      for (let key of Object.keys(el.attributes)) {
        let attr = el.attributes[key];
        if (['class', 'id'].includes(attr.name) === false) {
          script.setAttribute(attr.name, attr.value);
        }
      }
      el.appendChild(script);
    });
  }
  window.addEventListener('DOMContentLoaded', (event) => {
    loadJS();
  });
</script>




<!-- inject -->


  </div>
</body>
</html>
